/*

The MIT License

Copyright (c) 2007 Clever Thinking (PrimetexT Ltd)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 */

package couk.cleverthinking.javamoney;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;

import org.postgresql.util.PGobject;

/**
 * Base class for javamoney routines. This wraps a BigDecimal which is the base
 * currency. Additionally it adds the equivalent local currency along with
 * currency code and exchange rate.
 * 
 * By replicating all the BigDecimal methods and applying them to base, this
 * class can be used as a drop-in replacement.
 * 
 * Two differences with BigDecimal... 1. This class is mutable 2. This class
 * accepts setting to null or empty string and interprets as zero
 * 
 * @author winz
 * 
 */
public class Money implements Serializable {

	private static final String DELIMITER = ",";

	private static final long serialVersionUID = 1370324617867387182L;

	private BigDecimal base = new BigDecimal("0.00"); // exchange rate used in
	// this conversion
	private BigDecimal foreign; // initial local currency
	private String iso4217 = null; // ISO 4217 char currency code
	private BigDecimal rate; // exchange rate used in this conversion
	private String name; // self awareness
	private String creator; // the creating class
	private boolean baseOnly=true;  // true if there is no foreign currency component  

	private final RoundingMode roundingMode = RoundingMode.HALF_EVEN;

	private final String formatError = " is malformed. Format must be one of 'base', 'foreign code', '(base:foreign:code:rate)', or 'base [foreign:code@rate]'.";

	public static Money ZEROforeign;
	public static Money ZERO;

	static {
		ZERO = new Money("0.00");
		ZEROforeign = new Money("0.00");
	}

	/**
	 * Because this version is mutable, allow a no-arg constructor which sets
	 * the value to 0.00
	 */
	// public Money() {
	// super("0.00"); // set the value to zero initially
	// // setScale(newScale, roundingMode); // TODO - need to confirm
	// appropriate values
	// }
	/**
	 * 
	 * Database Object setter. The Object will be a BigDecimal if the database
	 * is a simple value, or a PGobject if it's a compound type
	 * 
	 * @param value
	 */
	public Money(Object _value) {
		// try a numeric/BigDecimal set first as this is most common
		try {
			base = (BigDecimal) _value;
			baseOnly=true;
		} catch (ClassCastException e) {
			// if that fails, try a PGobject
			if (e.toString().indexOf("PGobject") > -1) {
				PGobject pgo = (PGobject) _value;
				setValue(pgo.getValue());
			} else {
				throw e;
			}
		}
	}

	/**
	 * Extends the BigDecimal(String) constructor to support additional string
	 * formats. See setValue(String) for details
	 * {@link couk.cleverthinking.javamoney.setValue} setValue for details
	 * 
	 * NB, Unlike BigDecimal, we interpret setting to null as zero. BigDecimal
	 * would throw NumberFormatException
	 * 
	 * @param The
	 *            string value to set
	 * 
	 */
	/**
	 * @param _value
	 */
	public Money(String _value) {
		setValue(_value);
		// set the SQL type based on whether this is a cmompound object with
		// foreign currency information
		// or a simple base currency
		if (iso4217 == null) {
			baseOnly=true;
		} else {
			baseOnly=false;
		}
	}

	// l(values[0]);
	// l(values[1]);
	// l(values[2]);
	// l(values[3]);

	@SuppressWarnings("unused")
	private void l(String value) {
		System.out.println(value);
	}

	/**
	 * Supported formats ... . null or empty string is interpreted as 0.00 .
	 * Numeric value as per BigDecimal (e.g. .01, -1.23). NB Such a value will
	 * be interpreted as the base currency amount . Numeric value with an ISO
	 * 4217 currency code (e.g. 12.7USD, 100 php, -12 GbP) NB Such a value will
	 * be interpreted as the foreign currency amount and will be converted to
	 * base currency . Money.toString output format. Our toString() method emits
	 * values as base [foreign ISO@rate], e.g. 2.00 [4.06:USD@2.03] We will
	 * accept such a string back . Full SQL Stream of
	 * (base,foreign,iso4217,rate) with optional quotes (e.g. (2,4.06,USD,2.03) .
	 * Partial SQL Stream of (base,,,) with optional quotes (e.g. (100,,,)
	 * 
	 * @param _value
	 */
	public void setValue(String _value) {
//		System.out.println("in setValue with value="+_value);
		if (_value == null || _value.length() == 0) {
			return; // by returning for a null the value defaults to zero
		}

		try {
			_value = _value.replaceAll("\\s", ""); // lose whitespace
//			System.out.println("in setValue with value="+_value);
			// First try for a simple numeric, this is the most common case
			try {
				base = new BigDecimal(_value);
				return;
			} catch (NumberFormatException e) {
			}

			// try SQL format
			if ((_value.startsWith("(") && _value.endsWith(")"))) {
				_value = _value.substring(1, _value.length() - 1);
				String values[] = _value.split(DELIMITER);

				base = new BigDecimal(values[0]);
				if (values.length > 1) { // it's possible to just have the
					// base value ie (12.34,,,)
					foreign = new BigDecimal(values[1]);
					iso4217 = values[2].toUpperCase();
					rate = new BigDecimal(values[3]);
				}
				setScale();
				return;
			}

			// try our toString format "base [foreign:iso@rate]"
			int i1, i2, i3;
			if (((i1 = _value.indexOf('[')) > -1) && _value.endsWith("]")
					&& (i2 = _value.indexOf(':')) > i1
					&& (i3 = _value.indexOf('@')) > i2) {
				base = new BigDecimal(_value.substring(0, i1));
				base = base.setScale(2);
				// add(new BigDecimal(_value.substring(0, i1)));
				foreign = new BigDecimal(_value.substring(i1 + 1, i2));
				iso4217 = _value.substring(i2 + 1, i3).toUpperCase();
				rate = new BigDecimal(_value.substring(i3 + 1,
						_value.length() - 1));
				setScale();
				return;
			}

			// from here we'll assume it's a "local iso" format and do a
			// conversion
			foreign = new BigDecimal(_value.replaceAll("[^0-9.-]", ""));
			foreign = foreign.setScale(2); // make sure we have the correct
											// precision
			iso4217 = _value.replaceAll("\\W", "").replaceAll("\\d", "")
					.toUpperCase();
			rate = ExchangeRates.getRate(iso4217);
			if (rate == null) {
				System.out.println("no exchange rate found for "+iso4217);
			}
			base = foreign.divide(rate, roundingMode); // use consistent
														// rounding
			setScale();
		} catch (Exception e) {
			 e.printStackTrace();
			throw new NumberFormatException(_value + formatError
					+ " Original exception was " + e.toString());
		}
	}

	/**
	 * Ensure that our values always have the correct scale - default 2
	 */
	private void setScale() {
		try {
			base = base.setScale(2);
		} catch (Exception e) {
		}
		try {
			foreign = foreign.setScale(2); // make sure we have the correct
											// precision
			baseOnly=false;
		} catch (Exception e) {
		}
	}

	/**
	 * Returns the value of this Money object in a way that is suitable for
	 * setting into a PGobject. Specifically this is (base,foreign,iso,rate)
	 * 
	 * @return
	 */
	public String getSqlValue() {
		return ("(" + base.toString() + DELIMITER + foreign + DELIMITER + iso4217 + DELIMITER
				+ rate + ")").replaceAll("null", "");
	}

	/**
	 * Output this value in a human readable form. If the foreign currency
	 * informationm is null, just output the base value. This allows Money to be
	 * used as a drop in replacement for BigDecimal.
	 * 
	 * If there if forex information present, this is included in the output in
	 * the format base [foreign :iso4217@rate] e.g. 2 [186 :PHP@93]
	 */
	public String toString() {
		if (foreign == null) {
			return base.toString();
		} else {
			return base.toString() + " [" + foreign + ":" + iso4217 + "@"
					+ rate + "]";
		}
	}

	/*
	 * Cloning stuff
	 */

	/**
	 * Duplicate this object.
	 * 
	 */
	public Money clone() {
		return new Money(this.toString());
	}

	/**
	 * If we do arithmetic on a Money object, any pre-existing foreign currency
	 * information is no longer vald so needs to be cleared down
	 */
	private void clearDown() {
		this.foreign = null;
		this.iso4217 = null;
		this.rate = null;
	}

	/*
	 * Replica Methods from BigNumber - all applied to base There are generally
	 * 2 versions of each method, one taking a BigDecimal argument and one
	 * taking a Money argument
	 */

	public Money subtract(Money value) {
		return subtract(value.base);
	}

	public Money subtract(BigDecimal value) {
		Money m = new Money(base.toString());
		m.clearDown();
		m.base.subtract(value);
		return m;
	}

	public Money add(BigDecimal value) {
		Money m = new Money(base.toString());
		m.clearDown();
		m.base.add(value);
		return m;
	}

	public Money add(Money value) {
		return add(value.base);
	}

	public int compareTo(BigDecimal value) {
		return base.compareTo(value);
	}

	public int compareTo(Money value) {
		return compareTo(value.base);
	}

	public String toPlainString() {
		return base.toPlainString();
	}

	/*
	 * End of BigDecimal replica methods
	 */

	public BigDecimal getBase() {
		return base;
	}

	public void setBase(BigDecimal base) {
		this.base = base;
	}

	public String getCreator() {
		return creator;
	}

	public void setCreator(String creator) {
		this.creator = creator;
	}

	public BigDecimal getForeign() {
		return foreign;
	}

	public String getIso4217() {
		return iso4217;
	}

	public BigDecimal getRate() {
		return rate;
	}

	public String getName() {
		return name;
	}

	public boolean isBaseOnly() {
		return baseOnly;
	}

	public void setBaseOnly(boolean baseOnly) {
		this.baseOnly = baseOnly;
	}


	// public void readSQL(SQLInput stream, String type) throws SQLException {
	// sql_type = type;
	// local = new BigDecimal(stream.readDouble());
	// currency = stream.readString();
	// rate = new BigDecimal(stream.readDouble());
	// base = new BigDecimal(stream.readDouble());
	// }
	//
	// public void writeSQL(SQLOutput stream) throws SQLException {
	// stream.writeDouble(local.doubleValue());
	// stream.writeString(currency);
	// stream.writeDouble(rate.doubleValue());
	// stream.writeDouble(base.doubleValue());
	// }

}
